﻿//==================================================
// Copyright (C) 2016-2017 Fanjia
// All rights reserved
// CLR 版本: 4.0.30319.42000
// 机器名称: ZHX
// 文 件 名: StringMetricHelper
// 创建时间: 2018/1/6 9:43:55
// 创 建 人: 张华鑫
//==================================================

using System;
using System.Text;

namespace FanjiaHotelPlatform.utils.Kits
{
    /// <summary>
    /// 用于计算字符串相似度
    /// </summary>
    public static class StringMetricHelper
    {

        /// <summary>
        /// returns a string buffer of characters from string1 within string2 if they are of a given
        /// distance seperation from the position in string1.
        /// </summary>
        /// <param name="firstWord">string one</param>
        /// <param name="secondWord">string two</param>
        /// <param name="distanceSep">separation distance</param>
        /// <returns>a string buffer of characters from string1 within string2 if they are of a given
        /// distance seperation from the position in string1</returns>
        private static StringBuilder GetCommonCharacters(string firstWord, string secondWord, int distanceSep)
        {
            StringBuilder returnCommons = new StringBuilder();
            StringBuilder copy = new StringBuilder(secondWord);
            for (int i = 0; i < firstWord.Length; i++)
            {
                char ch = firstWord[i];
                bool foundIt = false;
                for (int j = Math.Max(0, i - distanceSep);
                    !foundIt && j < Math.Min(i + distanceSep, secondWord.Length);
                    j++)
                {
                    if (copy[j] == ch)
                    {
                        foundIt = true;
                        returnCommons.Append(ch);
                        copy[j] = '#';
                    }
                }
            }
            return returnCommons;
        }

        /// <summary>
        /// gets the similarity of the two strings using Jaro distance.
        /// </summary>
        /// <param name="firstWord"></param>
        /// <param name="secondWord"></param>
        /// <returns>a value between 0-1 of the similarity</returns>
        private static double GetJaroSimilarity(string firstWord, string secondWord)
        {
            //get half the length of the string rounded up - (this is the distance used for acceptable transpositions)
            int halflen = Math.Max(firstWord.Length, secondWord.Length) / 2 + 1;
            //get common characters
            StringBuilder common1 = GetCommonCharacters(firstWord, secondWord, halflen);
            int commonMatches = common1.Length;
            //check for zero in common
            if (commonMatches == 0)
            {
                return 0;
            }
            StringBuilder common2 = GetCommonCharacters(secondWord, firstWord, halflen);
            //check for same length common strings returning 0.0f is not the same
            if (commonMatches != common2.Length)
            {
                return 0;
            }
            //get the number of transpositions
            int transpositions = 0;
            for (int i = 0; i < commonMatches; i++)
            {
                if (common1[i] != common2[i])
                {
                    transpositions++;
                }
            }

            //calculate jaro metric
            transpositions /= 2;
            double tmp1;
            tmp1 = commonMatches / (3.0 * firstWord.Length) + commonMatches / (3.0 * secondWord.Length) +
                   (commonMatches - transpositions) / (3.0 * commonMatches);
            return tmp1;
        }

        /// <summary>
        /// gets the prefix length found of common characters at the begining of the strings.
        /// </summary>
        /// <param name="firstWord"></param>
        /// <param name="secondWord"></param>
        /// <returns>the prefix length found of common characters at the begining of the strings</returns>
        public static int GetPrefixLength(string firstWord, string secondWord)
        {
            int n = Math.Min(Math.Min(firstWord.Length, secondWord.Length), 4);
            for (int i = 0; i < n; i++)
            {
                if (firstWord[i] != secondWord[i])
                {
                    return i;
                }
            }
            return n;
        }

        /// <summary>
        /// 清理字符串
        /// </summary>
        /// <param name="source">源字符串</param>
        /// <returns>清理后的字符串</returns>
        public static string Clean(string source)
        {
            return string.IsNullOrEmpty(source)
                ? source
                : source.Replace(" ", "").Replace("　", "").Replace("(", "").Replace(")", "").Replace("（", "")
                    .Replace("）", "").Replace("【", "").Replace("】", "").Replace("-", "").Replace("—", "")
                    .Replace("·", "");
        }

        /// <summary>
        /// 获取 Levenstein 相似度
        /// </summary>
        /// <param name="source">源字符串</param>
        /// <param name="target">目标字符串</param>
        /// <returns>Levenstein 相似度</returns>
        public static double GetLevensteinSimilarity(string source, string target)
        {
            if (source == null || target == null)
            {
                return 0;
            }

            var s = Clean(source);
            var t = Clean(target);
            var n = s.Length;
            var m = t.Length;
            if (n == 0)
            {
                return m;
            }
            if (m == 0)
            {
                return n;
            }

            var d = new int[n + 1, m + 1];
            for (var i = 0; i <= n; d[i, 0] = i++)
            {
            }
            for (var j = 1; j <= m; d[0, j] = j++)
            {
            }
            for (var i = 1; i <= n; i++)
            {
                for (var j = 1; j <= m; j++)
                {
                    var cost = (target[j - 1] == source[i - 1]) ? 0 : 1;
                    var min1 = d[i - 1, j] + 1;
                    var min2 = d[i, j - 1] + 1;
                    var min3 = d[i - 1, j - 1] + cost;
                    d[i, j] = Math.Min(Math.Min(min1, min2), min3);
                }
            }
            return 1 - Convert.ToDouble(d[n, m]) / Math.Max(n, m);
        }

        /// <summary>
        /// 获取 JaroWinkler 相似度
        /// </summary>
        /// <param name="source">源字符串</param>
        /// <param name="target">目标字符串</param>
        /// <returns></returns>
        public static double GetJaroWinklerSimilarity(string source, string target)
        {
            if (string.IsNullOrWhiteSpace(source) || string.IsNullOrWhiteSpace(target))
            {
                return 0;
            }

            var s = Clean(source);
            var t = Clean(target);
            if (s == t)
            {
                return 1;
            }

            var jaro = GetJaroSimilarity(s, t);
            var prefixLength = GetPrefixLength(s, t);
            return jaro + prefixLength * 0.1 * (1 - jaro);
        }
    }
}
